/*!
- * OOjs UI v0.1.0-pre (da4b0d5c14)
+ * OOjs UI v0.1.0-pre (eee616d664)
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2014 OOjs Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2014-10-30T00:10:09Z
+ * Date: 2014-10-31T23:23:56Z
*/
( function ( OO ) {
*
* @static
* @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
- * @return {string} Text direction, either `ltr` or `rtl`
+ * @return {string} Text direction, either 'ltr' or 'rtl'
*/
OO.ui.Element.getDir = function ( obj ) {
var isDoc, isWin;
/**
* Get indicator name.
*
- * @return {string} title Symbolic name of indicator
+ * @return {string} Symbolic name of indicator
*/
OO.ui.IndicatorElement.prototype.getIndicator = function () {
return this.indicator;
/**
* Get the label.
*
- * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
* text; or null for no label
*/
OO.ui.LabelElement.prototype.getLabel = function () {
/**
* Get the names of all flags set.
*
- * @return {string[]} flags Flag names
+ * @return {string[]} Flag names
*/
OO.ui.FlaggedElement.prototype.getFlags = function () {
return Object.keys( this.flags );
*
* Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
*
+ * Subclasses that set the value of #lookupInput from their `choose` or `select` handler should
+ * be aware that this will cause new suggestions to be looked up for the new value. If this is
+ * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
+ *
* @class
* @abstract
*
this.lookupCache = {};
this.lookupQuery = null;
this.lookupRequest = null;
- this.populating = false;
+ this.lookupsDisabled = false;
+ this.lookupInputFocused = false;
// Events
this.lookupInput.$input.on( {
mousedown: this.onLookupInputMouseDown.bind( this )
} );
this.lookupInput.connect( this, { change: 'onLookupInputChange' } );
+ this.lookupMenu.connect( this, { toggle: 'onLookupMenuToggle' } );
// Initialization
this.$element.addClass( 'oo-ui-lookupWidget' );
* @param {jQuery.Event} e Input focus event
*/
OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
- this.openLookupMenu();
+ this.lookupInputFocused = true;
+ this.populateLookupMenu();
};
/**
* @param {jQuery.Event} e Input blur event
*/
OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
- this.lookupMenu.toggle( false );
+ this.closeLookupMenu();
+ this.lookupInputFocused = false;
};
/**
* @param {jQuery.Event} e Input mouse down event
*/
OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
- this.openLookupMenu();
+ // Only open the menu if the input was already focused.
+ // This way we allow the user to open the menu again after closing it with Esc
+ // by clicking in the input. Opening (and populating) the menu when initially
+ // clicking into the input is handled by the focus handler.
+ if ( this.lookupInputFocused ) {
+ this.openLookupMenu();
+ }
};
/**
* @param {string} value New input value
*/
OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
- this.openLookupMenu();
+ if ( this.lookupInputFocused ) {
+ this.populateLookupMenu();
+ }
+};
+
+/**
+ * Handle the lookup menu being shown/hidden.
+ * @param {boolean} visible Whether the lookup menu is now visible.
+ */
+OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
+ if ( !visible ) {
+ // When the menu is hidden, abort any active request and clear the menu.
+ // This has to be done here in addition to closeLookupMenu(), because
+ // MenuWidget will close itself when the user presses Esc.
+ this.abortLookupRequest();
+ this.lookupMenu.clearItems();
+ }
};
/**
};
/**
- * Open the menu.
+ * Disable or re-enable lookups.
+ *
+ * When lookups are disabled, calls to #populateLookupMenu will be ignored.
+ *
+ * @param {boolean} disabled Disable lookups
+ */
+OO.ui.LookupInputWidget.prototype.setLookupsDisabled = function ( disabled ) {
+ this.lookupsDisabled = !!disabled;
+};
+
+/**
+ * Open the menu. If there are no entries in the menu, this does nothing.
*
* @chainable
*/
OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
- var value = this.lookupInput.getValue();
-
- if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
- this.populateLookupMenu();
+ if ( !this.lookupMenu.isEmpty() ) {
this.lookupMenu.toggle( true );
- } else {
- this.lookupMenu
- .clearItems()
- .toggle( false );
}
+ return this;
+};
+/**
+ * Close the menu, empty it, and abort any pending request.
+ *
+ * @chainable
+ */
+OO.ui.LookupInputWidget.prototype.closeLookupMenu = function () {
+ this.lookupMenu.toggle( false );
+ this.abortLookupRequest();
+ this.lookupMenu.clearItems();
return this;
};
/**
- * Populate lookup menu with current information.
+ * Request menu items based on the input's current value, and when they arrive,
+ * populate the menu with these items and show the menu.
+ *
+ * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
*
* @chainable
*/
OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
- var widget = this;
+ var widget = this,
+ value = this.lookupInput.getValue();
- if ( !this.populating ) {
- this.populating = true;
+ if ( this.lookupsDisabled ) {
+ return;
+ }
+
+ // If the input is empty, clear the menu
+ if ( value === '' ) {
+ this.closeLookupMenu();
+ // Skip population if there is already a request pending for the current value
+ } else if ( value !== this.lookupQuery ) {
this.getLookupMenuItems()
.done( function ( items ) {
widget.lookupMenu.clearItems();
.addItems( items )
.toggle( true );
widget.initializeLookupMenuSelection();
- widget.openLookupMenu();
} else {
- widget.lookupMenu.toggle( true );
+ widget.lookupMenu.toggle( false );
}
- widget.populating = false;
} )
.fail( function () {
widget.lookupMenu.clearItems();
- widget.populating = false;
} );
}
};
/**
- * Set selection in the lookup menu with current information.
+ * Select and highlight the first selectable item in the menu.
*
* @chainable
*/
* Get lookup menu items for the current query.
*
* @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
- * of the done event
+ * of the done event. If the request was aborted to make way for a subsequent request,
+ * this promise will not be rejected: it will remain pending forever.
*/
OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
var widget = this,
value = this.lookupInput.getValue(),
- deferred = $.Deferred();
+ deferred = $.Deferred(),
+ ourRequest;
- if ( value && value !== this.lookupQuery ) {
- // Abort current request if query has changed
- if ( this.lookupRequest ) {
- this.lookupRequest.abort();
- this.lookupQuery = null;
- this.lookupRequest = null;
- }
- if ( value in this.lookupCache ) {
- deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
- } else {
- this.lookupQuery = value;
- this.lookupRequest = this.getLookupRequest()
- .always( function () {
+ this.abortLookupRequest();
+ if ( value in this.lookupCache ) {
+ deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
+ } else {
+ this.lookupInput.pushPending();
+ this.lookupQuery = value;
+ ourRequest = this.lookupRequest = this.getLookupRequest();
+ ourRequest
+ .always( function () {
+ // We need to pop pending even if this is an old request, otherwise
+ // the widget will remain pending forever.
+ // TODO: this assumes that an aborted request will fail or succeed soon after
+ // being aborted, or at least eventually. It would be nice if we could popPending()
+ // at abort time, but only if we knew that we hadn't already called popPending()
+ // for that request.
+ widget.lookupInput.popPending();
+ } )
+ .done( function ( data ) {
+ // If this is an old request (and aborting it somehow caused it to still succeed),
+ // ignore its success completely
+ if ( ourRequest === widget.lookupRequest ) {
widget.lookupQuery = null;
widget.lookupRequest = null;
- } )
- .done( function ( data ) {
widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
- } )
- .fail( function () {
+ }
+ } )
+ .fail( function () {
+ // If this is an old request (or a request failing because it's being aborted),
+ // ignore its failure completely
+ if ( ourRequest === widget.lookupRequest ) {
+ widget.lookupQuery = null;
+ widget.lookupRequest = null;
deferred.reject();
- } );
- this.pushPending();
- this.lookupRequest.always( function () {
- widget.popPending();
+ }
} );
- }
}
return deferred.promise();
};
/**
- * Get a new request object of the current lookup query value.
- *
- * @abstract
- * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
+ * Abort the currently pending lookup request, if any.
*/
-OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
- // Stub, implemented in subclass
- return null;
+OO.ui.LookupInputWidget.prototype.abortLookupRequest = function () {
+ var oldRequest = this.lookupRequest;
+ if ( oldRequest ) {
+ // First unset this.lookupRequest to the fail handler will notice
+ // that the request is no longer current
+ this.lookupRequest = null;
+ this.lookupQuery = null;
+ oldRequest.abort();
+ }
};
/**
- * Handle successful lookup request.
- *
- * Overriding methods should call #populateLookupMenu when results are available and cache results
- * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
+ * Get a new request object of the current lookup query value.
*
* @abstract
- * @param {Mixed} data Response from server
+ * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
*/
-OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
+OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
// Stub, implemented in subclass
+ return null;
};
/**
};
/**
- * Icon widget.
- *
- * See OO.ui.IconElement for more information.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IconElement
- * @mixins OO.ui.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.IconWidget = function OoUiIconWidget( config ) {
- // Config intialization
- config = config || {};
-
- // Parent constructor
- OO.ui.IconWidget.super.call( this, config );
-
- // Mixin constructors
- OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
- OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-iconWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
-
-/* Static Properties */
-
-OO.ui.IconWidget.static.tagName = 'span';
-
-/**
- * Indicator widget.
+ * Dropdown menu of options.
*
- * See OO.ui.IndicatorElement for more information.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IndicatorElement
- * @mixins OO.ui.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
- // Config intialization
- config = config || {};
-
- // Parent constructor
- OO.ui.IndicatorWidget.super.call( this, config );
-
- // Mixin constructors
- OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
- OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-indicatorWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
-
-/* Static Properties */
-
-OO.ui.IndicatorWidget.static.tagName = 'span';
-
-/**
- * Inline menu of options.
- *
- * Inline menus provide a control for accessing a menu and compose a menu within the widget, which
+ * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which
* can be accessed using the #getMenu method.
*
* Use with OO.ui.MenuItemWidget.
* @param {Object} [config] Configuration options
* @cfg {Object} [menu] Configuration options to pass to menu widget
*/
-OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
+OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
// Configuration initialization
config = $.extend( { indicator: 'down' }, config );
// Parent constructor
- OO.ui.InlineMenuWidget.super.call( this, config );
+ OO.ui.DropdownWidget.super.call( this, config );
// Mixin constructors
OO.ui.IconElement.call( this, config );
// Initialization
this.$handle
- .addClass( 'oo-ui-inlineMenuWidget-handle' )
+ .addClass( 'oo-ui-dropdownWidget-handle' )
.append( this.$icon, this.$label, this.$indicator );
this.$element
- .addClass( 'oo-ui-inlineMenuWidget' )
+ .addClass( 'oo-ui-dropdownWidget' )
.append( this.$handle, this.menu.$element );
};
/* Setup */
-OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatorElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabelElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
+OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
/* Methods */
*
* @return {OO.ui.MenuWidget} Menu of widget
*/
-OO.ui.InlineMenuWidget.prototype.getMenu = function () {
+OO.ui.DropdownWidget.prototype.getMenu = function () {
return this.menu;
};
*
* @param {OO.ui.MenuItemWidget} item Selected menu item
*/
-OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
+OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
var selectedLabel;
if ( !item ) {
*
* @param {jQuery.Event} e Mouse click event
*/
-OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
+OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
// Skip clicks within the menu
if ( $.contains( this.menu.$element[0], e.target ) ) {
return;
return false;
};
+/**
+ * Icon widget.
+ *
+ * See OO.ui.IconElement for more information.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.IconWidget = function OoUiIconWidget( config ) {
+ // Config intialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.IconWidget.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
+ OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-iconWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IconWidget.static.tagName = 'span';
+
+/**
+ * Indicator widget.
+ *
+ * See OO.ui.IndicatorElement for more information.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IndicatorElement
+ * @mixins OO.ui.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
+ // Config intialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.IndicatorWidget.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
+ OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-indicatorWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IndicatorWidget.static.tagName = 'span';
+
/**
* Base class for input widgets.
*
* @param {Object} [config] Configuration options
* @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
* @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
- * if you need IE 6 support in a form with multiple buttons. By using this option, you sacrifice
- * icons and indicators, as well as the ability to have non-plaintext label or a label different
- * from the value.
+ * if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
+ * indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
+ * won't be possible to set a value (which will internally become identical to the label).
*/
OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
// Configuration initialization